/*
 * jQuery plugin to manage the URI anchor component ("hash fragmant")
 *
 * Copyright (c) 2013-2015 Michael S. Mikowski
 * (mike[dot]mikowski[at]gmail[dotcom])
 *
 * Dual licensed under the MIT or GPL Version 2
 * http://jquery.org/license
 *
 * Versions
 *  1.1.1-1.1.3 - Initial jQuery plugin site releases
 *  1.2.1-1.3.3 - Updated documentation, minor bug fixes
*/
/*jslint         browser : true, continue : true,
  devel  : true, indent  : 2,    maxerr   : 50,
  newcap : true, nomen   : true, plusplus : true,
  regexp : true, sloppy  : true, vars     : false,
  white  : true
*/
/*global jQuery */

(function ($) {
    $.uriAnchor = (function () {
      //---------------- BEGIN MODULE SCOPE VARIABLES --------------
      var
        configMap = {
          clean0_regex     : /^[#!]*/,
          clean1_regex     : /\?[^?]*$/,
          settable_map_key : { schema_map : true },
          schema_map       : null
        },
  
        getErrorReject,   getVarType,       getCleanAnchorString,
        parseStringToMap, makeAnchorString, setAnchor,
        makeAnchorMap,    configModule
        ;
      //----------------- END MODULE SCOPE VARIABLES ---------------
  
      //------------------- BEGIN UTILITY METHODS ------------------
      getErrorReject = function ( message  ) {
        var error     = new Error();
        error.name    = 'Anchor Schema Reject';
        error.message = message;
        return error;
      };
  
      // Begin public method /getVarType/
      // Returns 'Object', 'Array', 'String', 'Number', 'Boolean', 'Undefined'
      getVarType = function ( data  ) {
        if ( data === undefined  ) { return 'Undefined'; }
        if ( data === null  ) { return 'Null'; }
        return {}.toString.call( data ).slice( 8, -1 );
      };
      // End public method /getVarType/
  
      // Begin internal utility to clean bookmark
      getCleanAnchorString = function () {
        return String( document.location.href.split('#')[1] || '' )
          // remove any leading pounds or bangs
          .replace( configMap.clean0_regex , '' )
          // snip off after question-mark ( a ClickStreet bug )
          .replace( configMap.clean1_regex , '' )
          ;
      };
      // End internal utility to clean bookmark
  
      // Begin internal utility /parseStringToMap/
      parseStringToMap = function ( arg_map  ) {
        var
          input_string    = arg_map.input_string    || '',
          delimit_char    = arg_map.delimit_char    || '&',
          delimit_kv_char = arg_map.delimit_kv_char || '=',
          output_map      = {},
  
          splitter_array, i, key_val_array
          ;
  
        splitter_array = input_string.split( delimit_char );
  
        for ( i = 0; i < splitter_array.length; i++  ) {
          key_val_array = splitter_array[i].split( delimit_kv_char );
  
          if ( key_val_array.length === 1  ) {
            output_map[decodeURIComponent( key_val_array[0] )] = true;
          }
          else if ( key_val_array.length === 2  ) {
            output_map[decodeURIComponent( key_val_array[0] )]
              = decodeURIComponent( key_val_array[1] )
              ;
          }
        }
        return output_map;
      };
      // End internal utility /parseStringToMap/
  
      // Begin utility /makeAnchorString/
      // -- all the heavy lifting for setAnchor ( see below )
      // Converts a map into the anchor component as described
      // in setAnchor
      makeAnchorString = function ( anchor_map_in, option_map_in  ) {
        var
          anchor_map          = anchor_map_in || {},
          option_map          = option_map_in || {},
          delimit_char        = option_map.delimit_char        || '&',
          delimit_kv_char     = option_map.delimit_kv_char     || '=',
          sub_delimit_char    = option_map.sub_delimit_char    || ':',
          dep_delimit_char    = option_map.dep_delimit_char    || '|',
          dep_kv_delimit_char = option_map.dep_kv_delimit_char || ',',
          schema_map         = configMap.schema_map,
          key_val_array       = [],
  
          schema_map_val, schema_map_dep, schema_map_dep_val,
          key_name, key_value, class_name, output_kv_string,
          sub_key_name, dep_map, dep_key_name, dep_key_value,
  
          dep_kv_array
          ;
  
        if ( getVarType( anchor_map ) !== 'Object' ) {
          return false;
        }
  
        for ( key_name in anchor_map  ) {
          // filter out inherited properties
          if ( anchor_map.hasOwnProperty( key_name ) ) {
  
            // skip empty and dependent keys
            if ( ! key_name  ) { continue;}
            if ( key_name.indexOf( '_' ) === 0 ) { continue;}
  
            // check against anchor schema if provided
            if ( schema_map  ) {
              if ( ! schema_map[key_name] ) {
                throw getErrorReject(
                  'Independent key |'
                  + key_name + '| not authorized by anchor schema'
                );
              }
            }
  
            output_kv_string   = '';
            key_value   = anchor_map[key_name];
  
            if ( key_value === undefined ) { key_value = ''; }
  
            class_name = getVarType( key_value  );
  
            // check against anchor schema map of allowable
            // values is provided
            if ( schema_map  ) {
              schema_map_val = schema_map[key_name];
              if ( getVarType( schema_map_val ) === 'Object'
                && ! schema_map_val[String( key_value )]
              ) {
                throw getErrorReject(
                  'Independent key-value pair |'
                  + key_name + '|' + String( key_value )
                  + '| not authorized by anchor schema'
                );
              }
            }
  
            // Booleans, we skip false
            if ( class_name === 'Boolean'  ) {
              if ( key_value ) { output_kv_string += encodeURIComponent( key_name ); }
            }
            // String and Number
            else {
               output_kv_string
                  += encodeURIComponent( key_name )
                  +  delimit_kv_char
                  +  encodeURIComponent( key_value )
                  ;
            }
  
            sub_key_name = '_' + key_name;
            if ( anchor_map.hasOwnProperty( sub_key_name ) ) {
              dep_map      = anchor_map[sub_key_name];
              dep_kv_array = [];
  
              if ( schema_map  ) {
                schema_map_dep = schema_map[sub_key_name];
                if ( ! schema_map_dep  ) {
                  throw getErrorReject(
                    'Dependent key |' + sub_key_name
                    + '| not authorized by anchor schema'
                  );
                }
              }
              else {
                schema_map_dep = null;
              }
  
              for ( dep_key_name in dep_map  ) {
                if ( dep_map.hasOwnProperty( dep_key_name ) ) {
                  dep_key_value = dep_map[dep_key_name];
  
                  if ( schema_map_dep  ) {
                    schema_map_dep_val = schema_map_dep[dep_key_name];
                    if ( getVarType( schema_map_dep_val ) === 'Object'
                      && ! schema_map_dep_val[String( dep_key_value )]
                    ) {
                      throw getErrorReject(
                        'Dependent key-value pair |'
                        + dep_key_name + '|' + String( dep_key_value )
                        + '| not authorized by anchor schema'
                      );
                    }
                  }
  
                  // Booleans, we skip false
                  if ( class_name === 'Boolean'  ) {
                    if ( dep_key_value === true  ) {
                      dep_kv_array.push( encodeURIComponent( dep_key_name ));
                    }
                  }
                  // String and Number
                  else {
                    dep_kv_array.push(
                      encodeURIComponent( dep_key_name )
                      + dep_kv_delimit_char
                      + encodeURIComponent( dep_key_value )
                    );
                  }
                }
              }
              // append dependent arguments if there are any
              if ( dep_kv_array.length > 0  ) {
                output_kv_string
                  += sub_delimit_char + dep_kv_array.join( dep_delimit_char )
                ;
              }
            }
            key_val_array.push( output_kv_string );
          }
        }
  
        return key_val_array.join( delimit_char );
      };
      // End utility /makeAnchorString/
      //-------------------- END UTILITY METHODS -------------------
  
      //------------------- BEGIN PUBLIC METHODS -------------------
      // Begin public method /setAnchor/
      // Purpose     :
      //   Sets Anchor component of the URI from a Map
      //   (The Anchor component is also known as the
      //   'hash fragment' or 'bookmark component')
      // Arguments  : positional -
      //   * 1 ( anchor_map )   : The map to be encoded to the URI anchor
      //   * 2 ( option_map )   : map of options
      //   * 3 ( replace_flag )  : boolean flag to replace the URI
      //     When true, the URI is replaced, which means the prior URI
      //     is not entered into the browser history
      // Environment : Expects the document.location browser object
      // Settings    : none
      // Returns     : boolean: true on success, false on failure
      // Throws      : none
      // Discussion  :
      //
      //  The first positional argument, anchor_map, may be a simple map:
      //    $.uriAnchor.setAnchor({
      //      page   : 'profile',
      //      slider : 'confirm',
      //      color  : 'red'
      //    });
      //
      //  This changes the URI anchor to:
      //     #!page=profile&slider=confirm&color=red
      //
      //  All these arguments are independent, that is, they can vary
      //  independent of each other. We also support dependent values -
      //  values that depend on others.
      //
      //  An independent argument key has no '_' prefix.  The same key name,
      //  prefixed by an '_', holds the arguments that are dependent on
      //  an independent argument.  The dependent key always points
      //  to a map.  Consider:
      //
      //    $.uriAnchor.setAnchor({
      //      page   : 'profile',
      //      _page  : {
      //        uname   : 'wendy',
      //        online  : 'today'
      //      }
      //    });
      //
      //  This changes the URI Anchor to:
      //    #!page=profile:uname,wendy|online,today
      //
      //  Only independent keys and their matching dependent keys are
      //  processed.  All other keys are ignored.  Importantly, this includes
      //  keys of the form _s_/key/ ( e.g. '_s_page' ) returned by makeAnchorMap
      //
      //  Setting a more complex anchor map is illustrated below:
      //    $.uriAnchor.setAnchor({
      //      page : 'profile',
      //      _page : {
      //        uname   : 'wendy',
      //        online  : 'today'
      //      },
      //      slider  : 'confirm',
      //      _slider : {
      //       text   : 'hello',
      //       pretty : false
      //      },
      //      color : 'red'
      //    });
      //
      //  This sets the URI Anchor to:
      //     #!page=profile:uname,wendy|online,today&slider=confirm:text,hello\
      //       |pretty,false&color=red
      //
      //   Options: The second positional argument tp this method, option_map,
      //   provides a number of options for delimiters:
      //     * delimit_char     : delimiter independent args
      //       Defaults to '&'
      //     * delimit_kv_char  : delimiter key-value of independent args
      //       Defaults to '='
      //     * sub_delimit_char : delimiter independent and dependent args
      //       Defaults to ':'
      //     * dep_delimit_char : delimiter between key-value of dependent args
      //       Defaults to '|'
      //     * dep_kv_delimit_char : key-value delimiter for dependent args.
      //       Defaults to ','
      //
      //   Boolean values ( as part of a key-value pair ) are convert into
      //     the stings 'true' or 'false'.
      //
      //  Validation:
      //
      //  As of 1.0, the ability to optionally check the validity of the
      //  Anchor against a schema has been included.  Since we don't expect
      //  the allowable schema to change during run-time, we use a
      //  module configuration to set the schema, like so:
      //
      //    $uriAnchor.configModule({
      //      schema_map : {
      //        page    : { profile : true, pdf : true },
      //        _page   : {
      //          uname   : true,
      //          online  : { 'today','yesterday','earlier' }
      //        },
      //        slider  : { confirm : 'deny' },
      //        _slider : { text : 'goodbye' },
      //        color   : { red : true, green : true, blue : true }
      //      }
      //    });
      //
      //  This check occurs only during setting of the Anchor, not
      //  during its parsing ( See makeAnchorMap )
      //
      //  The replace_flag instructs the routine to replace the uri,
      //  discarding browser history
      //
      setAnchor = function ( anchor_map, option_map, replace_flag  ) {
        var
          anchor_string = makeAnchorString( anchor_map, option_map  ),
          uri_array, uri_string
          ;
  
        uri_array = document.location.href.split( '#',2 );
        uri_string = anchor_string
          ? uri_array[0] + '#!' + anchor_string : uri_array[0]
          ;
  
        if ( replace_flag  ) {
          if ( anchor_string  ) {
            document.location.replace( uri_array[0] + '#!' + anchor_string );
          }
          else {
            document.location.replace( uri_array[0] );
          }
          return true;
        }
        // we replace the full href so that jquery recognizes the uri
        // change
        document.location.href = uri_string;
      };
      // End public method /setAnchor/
  
      // Begin public method /makeAnchorMap/
      // Purpose     : Parses URI anchor and returns as map
      // Arguments  : none
      // Environment : Expects the document.location browser object
      // Settings    : none
      // Returns     : Map
      // Throws      : none
      //
      // Discussion :
      //   Parses the browser URI anchor into a map using the same
      //   rules used to set the anchor in the method setAnchor
      //   ( see above ).
      //
      //   This method creates an additional key type, _s_<indendent_arg>
      //   for each independent argument with dependent arguments.
      //
      //   These keys point to a string representation of the independent
      //   argument along with all its dependent arguments.
      //
      //   These values are ignored by setAnchor, but they are useful
      //   for routines using setAnchor to check if a part of the anchor
      //   has changed.
      //
      // Example:
      //   If the browser URI Anchor looks like this:
      //     #!page=profile:uname,wendy|online,true&slider=confirm:text,hello\
      //     |pretty,false&color=red
      //
      //   Then calling $.uriAnchor.makeAnchorMap();
      //   will return a map that looks like so:
      //
      //     { page : 'profile',
      //       _page : {
      //         uname   : 'wendy',
      //         online  : 'today'
      //       },
      //       _s_page : 'profile:uname,wendy|online,today',
      //       slider  : 'confirm',
      //       _slider : {
      //        text   : 'hello',
      //        pretty : false
      //       },
      //       _s_slider : 'confirm:text,hello|pretty,false',
      //       color : 'red'
      //     };
      //
  
      makeAnchorMap = function () {
        var
          anchor_string = getCleanAnchorString(),
          anchor_map, idx, keys_array, key_name, key_value, dep_array
          ;
  
        if ( anchor_string === ''  ) { return {}; }
  
        // first pass decompose
        anchor_map = parseStringToMap({
          input_string     : anchor_string,
          delimit_char     : '&',
          delimit_kv_char  : '='
        });
  
        // extract keys to prevent run-away recursion when
        // adding keys to anchor_map, below
        keys_array = [];
        for ( key_name in anchor_map  ) {
          if ( anchor_map.hasOwnProperty( key_name ) ) {
            keys_array.push( key_name );
          }
        }
  
        for ( idx = 0; idx < keys_array.length; idx++  ) {
          key_name  = keys_array[idx];
          key_value = anchor_map[key_name];
  
          if ( getVarType( key_value ) !== 'String' || key_name === ''
          ) { continue; }
  
          // include string representation with all dependent keys and values
          anchor_map[ '_s_' + key_name ] = key_value;
  
          dep_array = key_value.split( ':' );
  
          if ( dep_array[1] && dep_array[1] !== '' ) {
            anchor_map[key_name] = dep_array[0];
  
            anchor_map[ '_' + key_name ] = parseStringToMap({
              input_string    : dep_array[1],
              delimit_char    : '|',
              delimit_kv_char : ','
            });
          }
        }
        return anchor_map;
      };
      // End public method /makeAnchorMap/
  
      // Begin public method /configModule/
      // Set configuration options
      configModule = function ( arg_map  ) {
        var
          settable_map = configMap.settable_map_key,
          key_name, error
          ;
  
        for ( key_name in arg_map ) {
          if ( arg_map.hasOwnProperty( key_name )) {
            if ( settable_map.hasOwnProperty( key_name )) {
              configMap[key_name] = arg_map[key_name];
            }
            else {
              error         = new Error();
              error.name    = 'Bad Input';
              error.message = 'Setting config key |' + key_name
                + '| is not supported';
              throw error;
            }
          }
        }
      };
      // End public method /configModule/
  
      // return public methods
      return {
        configModule     : configModule,
        getVarType       : getVarType,
        makeAnchorMap    : makeAnchorMap,
        makeAnchorString : makeAnchorString,
        setAnchor        : setAnchor
      };
      //------------------- END PUBLIC METHODS ---------------------
    }());
  } ( jQuery ));